export class WaterfallUtils {
  private container: HTMLElement;

  constructor(container: string | HTMLElement) {
    if (typeof container === 'string') {
      const el = document.querySelector<HTMLElement>(container);
      if (!el) throw new Error(`Container "${container}" not found`);
      this.container = el;
    } else {
      this.container = container;
    }
  }

  public layout() {
    const container = this.container;
    const els = container.children;

    function style(el: Element) {
      return window.getComputedStyle(el);
    }

    function margin(name: string, el: Element) {
      return parseFloat(style(el)['margin' + name as keyof CSSStyleDeclaration]) || 0;
    }

    function px(n: number) {
      return n + 'px';
    }

    function y(el: HTMLElement) {
      return parseFloat(el.style.top || '0');
    }

    function x(el: HTMLElement) {
      return parseFloat(el.style.left || '0');
    }

    function width(el: Element) {
      return parseFloat(style(el).width);
    }

    function height(el: Element) {
      return parseFloat(style(el).height);
    }

    function bottom(el: HTMLElement) {
      return y(el) + height(el) + margin('Bottom', el);
    }

    function right(el: HTMLElement) {
      return x(el) + width(el) + margin('Right', el);
    }

    function sort(l: HTMLElement[]) {
      l.sort((a, b) => {
        const diff = bottom(b) - bottom(a);
        return diff || x(b) - x(a);
      });
    }

    class Boundary {
      private els: HTMLElement[];

      constructor(firstRow: HTMLElement[]) {
        this.els = firstRow;
        sort(this.els);
      }

      add(el: HTMLElement) {
        this.els.push(el);
        sort(this.els);
        this.els.pop();
      }

      min() {
        return this.els[this.els.length - 1];
      }

      max() {
        return this.els[0];
      }
    }

    function placeEl(el: HTMLElement, top: number, left: number) {
      el.style.position = 'absolute';
      el.style.top = px(top);
      el.style.left = px(left);
    }

    function placeFirstElement(el: HTMLElement) {
      placeEl(el, 0, margin('Left', el));
    }

    function placeAtTheFirstLine(prev: HTMLElement, el: HTMLElement) {
      placeEl(el, y(prev), right(prev) + margin('Left', el));
    }

    function placeAtTheSmallestColumn(minEl: HTMLElement, el: HTMLElement) {
      placeEl(el, bottom(minEl) + margin('Top', el), x(minEl));
    }

    function adjustContainer(container: HTMLElement, maxEl: HTMLElement) {
      container.style.position = 'relative';
      container.style.height = px(bottom(maxEl) + margin('Bottom', maxEl));
    }

    function thereIsSpace(els: HTMLCollection, i: number) {
      return right(els[i - 1] as HTMLElement) + width(els[i]) <= width(container);
    }

    if (!els.length) return;

    placeFirstElement(els[0] as HTMLElement);

    let i = 1;
    for (; i < els.length && thereIsSpace(els, i); i++) {
      placeAtTheFirstLine(els[i - 1] as HTMLElement, els[i] as HTMLElement);
    }

    const firstRow = Array.from(els).slice(0, i) as HTMLElement[];
    const boundary = new Boundary(firstRow);

    for (; i < els.length; i++) {
      placeAtTheSmallestColumn(boundary.min(), els[i] as HTMLElement);
      boundary.add(els[i] as HTMLElement);
    }

    adjustContainer(container, boundary.max());
  }
}
